﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Controls;
using GalaSoft.MvvmLight.Messaging;
using SessionHunter.Business;
using SessionHunter.Business.Exceptions;
using SessionHunter.Business.SessionReading;
using SessionHunter.DataAccess;
using SessionHunter.Messages;
using SessionHunter.Model;
using SessionHunter.Resources;
using SessionHunter.Utilities;
using SessionHunter.Views;

namespace SessionHunter.ViewModel
{
    /// <summary>
    /// The main view model, which is the main interface and stores references to all of the major repositories and managers we want to keep in memory.
    /// </summary>
    public partial class MainViewModel : ViewModelBaseWithNotifications<MainViewModelMessage, MainViewModelMessage.MessageAction>
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        #region Constants
        /// <summary>
        /// How many errors to display in the error list at the bottom
        /// </summary>
        private int _numberOfErrorsToDisplay = 10;
        /// <summary>
        /// The number of pixels used for the error row.  We start with 0 until an error is raised.
        /// </summary>
        private int _errorRowPixels = 100;
        #endregion

        #region Variables
        /// <summary>
        /// Allows access to the current sessions in the database
        /// </summary>
        private SessionRepository _sessionRepository;
        /// <summary>
        /// Provides a way to get/save options to isolated storage
        /// </summary>
        private ApplicationRepository _applicationRepository;
        /// <summary>
        /// Manages the connection strings
        /// </summary>
        private ConnectionStringRepository _connectionStringRepository;
        /// <summary>
        /// Manages all of the existing references to assemblies
        /// </summary>
        private ReferenceManager _referenceManager;
        /// <summary>
        /// Stores the current options
        /// </summary>
        private ApplicationOptions _options;
        /// <summary>
        /// A list of assemblies that cannot be resolved when trying to deserialize the data
        /// </summary>
        private ObservableCollection<string> _missingAssemblies;
        /// <summary>
        /// Keep track of the references view, if it's already open
        /// </summary>
        private static ReferencesView _viewReferences = null;
        /// <summary>
        /// A list of errors we are currently displaying.  Treated as a LIFO list
        /// </summary>
        private List<string> _errors = null;
        #endregion

        /// <summary>
        /// Initializes a new instance of the MainViewModel class.  Used by unit tests.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public MainViewModel()
            : this(null)
        {

        }

        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        /// <param name="applicationRepository">The application repository.</param>
        /// <param name="sessionRepository">The session repository.</param>
        /// <param name="connectionStringRepository">The connection string repository.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public MainViewModel(ApplicationRepository applicationRepository, SessionRepository sessionRepository = null, ConnectionStringRepository connectionStringRepository = null)
        {
            if (!IsInDesignMode)
            {
                logger.Info("Loading MainViewModel - Standard Mode");

                // Have the application react whenever application settings change
                Messenger.Default.Register<ApplicationOptionsChangedMessage>(this, ApplyNewApplicationSettings);

                // Controls the busy indicator
                Messenger.Default.Register<BusyIndicatorMessage>(this, BusyIndicatorMessageReceived);

                PrepareRepositories(applicationRepository, sessionRepository, connectionStringRepository);

                ConfigureRelayCommands();
            }
            else
            {
                logger.Info("Loading MainViewModel - Design Mode");
                // Code runs in Blend --> create design time data.
            }

        }

        /// <summary>
        /// Prepares all of the repositories we keep instantiated while the VM is active.
        /// </summary>
        /// <param name="applicationRepository">The application repository.</param>
        /// <param name="sessionRepository">The session repository.</param>
        /// <param name="connectionStringRepository">The connection string repository.</param>
        private void PrepareRepositories(ApplicationRepository applicationRepository, SessionRepository sessionRepository, ConnectionStringRepository connectionStringRepository)
        {
            if (applicationRepository != null)
            {
                _applicationRepository = applicationRepository;
                // Load any existing application options or load a default
                Options = _applicationRepository.GetOptions();
                _referenceManager = new ReferenceManager();
                _referenceManager.AddDynamicReferences(Options.ReferencePaths);
            }

            if (connectionStringRepository != null)
            {
                _connectionStringRepository = connectionStringRepository;
            }

            if (sessionRepository != null)
            {
                _sessionRepository = sessionRepository;
                try
                {
                    PopulateSessions();
                }
                catch (Exception ex)
                {
                    // If there is an error log it, but then clear out the session repository because there is an issue with it.  This should also force us to ask for a new connection string.
                    Helper.HandleException(logger, "There was an error retrieving the session data from the database", ex);
                    _sessionRepository = null;
                }
            }
        }

        /// <summary>
        /// Populates the list of sessions aynchronously, based on the options.
        /// </summary>
        private void PopulateSessions()
        {
            if (Options != null)
            {
                using (var worker = new BackgroundWorker())
                {

                    worker.DoWork += (o, ea) =>
                    {
                        try
                        {
                            logger.Info("Populating sessions");
                            Sessions = new ObservableCollection<SessionData>(_sessionRepository.GetCurrentSessions(Options));
                        }
                        catch (SessionApplicationException sae)
                        {
                            var uiHelper = new UIHelper(true);
                            uiHelper.HandleException(logger, "Error populating sessions", sae);
                        }

                    };

                    worker.RunWorkerCompleted += (o, ea) =>
                    {
                        // Make sure we turn off the busy indicator in every circumstance, or it might stay up forever
                        Messenger.Default.Send(new BusyIndicatorMessage(false));
                    };

                    Messenger.Default.Send(new BusyIndicatorMessage(true, Strings.Busy_GettingSessionData));
                    worker.RunWorkerAsync();
                }
            }
        }

        /// <summary>
        /// Called when the main view has finished loading
        /// </summary>
        private void ViewLoaded()
        {
            // We didn't load the session repository, so prompt them to put one in
            if (_sessionRepository == null)
            {
                ChangeDatabaseConnection();
            }
        }

        /// <summary>
        /// Exits the application, performing any cleanup.
        /// </summary>
        public void ShutdownApplication()
        {
            logger.Info("Closing Session Hunter");
            Application.Current.Shutdown();
        }

        /// <summary>
        /// When messages are raised by the various ViewModels, they are received here to be processed
        /// </summary>
        /// <param name="msg">The message received.</param>
        protected override void NotificationMessageReceived(MainViewModelMessage msg)
        {
            logger.Info("MainViewModel received message {0}", msg.Message.ToString());

            switch (msg.Message)
            {
                case MainViewModelMessage.MessageAction.OpenReferencesView:
                    if (_viewReferences == null)
                    {
                        _viewReferences = new ReferencesView();
                        Messenger.Default.Send<OpenWindowMessage>(new OpenWindowMessage(_viewReferences));
                    }
                    else
                    {
                        _viewReferences.Activate();
                    }
                    break;
                case MainViewModelMessage.MessageAction.CloseReferencesView:
                    if (_viewReferences != null)
                    {
                        _viewReferences.Close();
                        _viewReferences = null;
                    }
                    break;
                case MainViewModelMessage.MessageAction.SaveReferencesToSettings:
                    _applicationRepository.SaveReferences(_referenceManager.CurrentReferences);
                    break;
                case MainViewModelMessage.MessageAction.OpenChangeDatabaseConnectionDialog:

                    var viewChangeDatabaseConnection = new ChangeDatabaseConnectionView();
                    Messenger.Default.Send<OpenWindowMessage>(new OpenWindowMessage(viewChangeDatabaseConnection, true));
                    var viewModel = viewChangeDatabaseConnection.DataContext as ChangeDatabaseConnectionViewModel;
                    break;
                case MainViewModelMessage.MessageAction.ConnectionStringChanged:
                    // This was called after we got the connection string on startup
                    var connectionString = ConnectionStringRepository.GetConnectionString();
                    if (String.IsNullOrEmpty(connectionString))
                        throw new SessionApplicationException("Tried to refresh the data, but there was no connection string defined");

                    logger.Debug("Creating session repository using connection string: " + connectionString);

                    _sessionRepository = new SessionRepository(connectionString);

                    try
                    {
                        PopulateSessions();
                    }
                    catch (Exception ex)
                    {
                        Helper.HandleException(logger, "There was an error retrieving the session data from the database", ex);
                    }
                    break;
                default:
                    throw new UnhandledCaseException(msg.Message);
            }
        }

        /// <summary>
        /// Runs the logic if we check the "show all session" checkbox
        /// </summary>
        /// <param name="setValue">if set to <c>true</c> then shows all sessions.</param>
        private void SetShowAllSessions(bool setValue)
        {
            if (setValue)
            {
                // Make the textbox empty
                NumberOfSessions = "";
                // Save the hardcoded value into the config file
                _applicationRepository.SaveOption<int>(ApplicationRepository.PersistedSettings.NumberOfSessions, ApplicationOptions.ShowAllSessionsValue);
            }
            else
            {
                //HACK: A small hack here where I just set it to default if we uncheck it
                NumberOfSessions = ApplicationOptions.DefaultNumberOfSessions.ToString();
                _applicationRepository.SaveOption<int>(ApplicationRepository.PersistedSettings.NumberOfSessions, ApplicationOptions.DefaultNumberOfSessions);
            }
        }


        /// <summary>
        /// We use this function to bind an enum to a radio button.  It will take the string value, defined in the XAML and
        /// convert it to the enum we specified and then apply it to the PersistedSettings
        /// </summary>
        /// <typeparam name="TEnum">The type of the setting bound in the radio button.</typeparam>
        /// <param name="persistedSetting">The persisted setting to save.</param>
        /// <param name="itemChanged">The string value, passed in the CommandParameter of the XAML.</param>
        private void SaveOptionByString<TEnum>(ApplicationRepository.PersistedSettings persistedSetting, string itemChanged) where TEnum : struct
        {
            TEnum valueToSet;

            // Try to parse it, and if we fail, we raise an exception.  It means the CommandParameter does not match an item in TEnum
            if (Enum.TryParse(itemChanged, out valueToSet))
            {
                _applicationRepository.SaveOption(persistedSetting, valueToSet);
            }
            else
                throw new UnhandledCaseException(itemChanged);
        }

        /// <summary>
        /// Populates the session list when you click on a specific session
        /// </summary>
        /// <param name="parameter">The <see cref="System.Windows.Controls.SelectionChangedEventArgs"/> instance containing the event data.</param>
        internal virtual void PopulateSessionItemsList(SelectionChangedEventArgs parameter)
        {
            Contract.Requires(parameter != null);
            Contract.Requires(parameter.AddedItems.Count <= 1, "More than one item selected");

            // Check to see if we have unselected the item in the list
            if (parameter.AddedItems.Count == 0)
            {
                // Clear the list
                SessionItems = null;
                return;
            }

            using (var worker = new BackgroundWorker())
            {
                worker.DoWork += (o, ea) =>
                {
                    var uiHelper = new UIHelper(true);

                    // get the raw data out so we can deserialize it
                    var item = (SessionData)parameter.AddedItems[0];

                    // Clear the session if we don't have valid data
                    if (item == null)
                    {
                        SessionItems = null;
                        return;
                    }

                    // Check for an empty session
                    var sessionItems = PopulateSessionItems(item, uiHelper);
                    if (sessionItems == null)
                    {
                        SessionItems = null;
                        return;
                    }

                    SessionItems = new ObservableCollection<SessionItem>(sessionItems);
                };

                worker.RunWorkerCompleted += (o, ea) =>
                {
                    // Make sure we turn off the busy indicator in every circumstance, or it might stay up forever
                    Messenger.Default.Send(new BusyIndicatorMessage(false));
                };

                Messenger.Default.Send(new BusyIndicatorMessage(true, Strings.Busy_ResolvingSessionData));
                worker.RunWorkerAsync();
            }
        }

        /// <summary>
        /// This will take the session data that was stored in the DB and break it down into
        /// individual items.
        /// </summary>
        /// <param name="item">The raw session data.</param>
        /// <param name="uiHelper">The UI helper.</param>
        /// <param name="sessionItems">A list of items in session.</param>
        /// <returns>
        /// True if we got sessions with no issue, or there were no sessions to retrieve.  False if there was an error.
        /// </returns>
        public virtual bool GetSessionItems(SessionData item, UIHelper uiHelper, out List<SessionItem> sessionItems)
        {
            sessionItems = null;

            try
            {
                var sessionHelper = new SessionHelper();
                sessionHelper.SessionItemsProcessed += SessionItemsProcessed;
                return sessionHelper.GetSessionItems(logger, item.Data, out sessionItems);
            }
            catch (Exception ex)
            {
                uiHelper.HandleException(logger, "Error getting and deserializing session items", ex);
            }
            return false;
        }

        /// <summary>
        /// Called whenver a number of session items have been processed
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The args.</param>
        void SessionItemsProcessed(object sender, SessionsProcessedArgs args)
        {
            logger.Info("Processed " + args.SessionsProcessed + " session items");
            if (args != null)
            {
                IsBusyMessage = string.Format(Strings.Busy_ResolvingSessionDataWithCounter, args.SessionsProcessed);
            }
        }

        /// <summary>
        /// Populates the session list when you click on a specific session
        /// </summary>
        /// <param name="item">The session item.</param>
        /// <param name="uiHelper">The UI helper.</param>
        /// <returns></returns>
        internal List<SessionItem> PopulateSessionItems(SessionData item, UIHelper uiHelper)
        {
            try
            {
                logger.Info("Called PopulateSessionItems");

                List<SessionItem> stateItemCollection = null;
                if (!GetSessionItems(item, uiHelper, out stateItemCollection))
                {
                    // Make the references button blink when there is an issue
                    uiHelper.MessengerSend(new MainWindowMessage(MainWindowMessage.MessageAction.References_Attention));

                    // There were errors with deserializing the data.  Populate the list of errors.
                    foreach (var stateItem in stateItemCollection)
                    {
                        if (!String.IsNullOrEmpty(stateItem.ErrorMessage))
                        {
                            // First see if it's a missing assembly message since that is the most common.  If not, show an error in the console at the bottom
                            string message = GetAssemblyName(stateItem.ErrorMessage);
                            if (message != null)
                            {
                                RaiseMissingAssembly(message);
                            }
                            else
                            {
                                AddErrorMessage(stateItem.ErrorMessage);
                            }
                        }
                    }
                }

                if (stateItemCollection != null && stateItemCollection.Count > 0)
                    return stateItemCollection;

            }
            catch (SerializationException se)
            {
                // We should not get here, because we caught the exception and wrote it to the SessionItem
                uiHelper.HandleException(logger, "Did not properly handle the error when deserializing the session", se);
            }
            catch (Exception ex)
            {
                uiHelper.HandleException(logger, "Error trying to reverse-engineer the session data", ex);
            }

            return null;
        }


        /// <summary>
        /// Processes the error if we are missing an assembly.  This will add it to the list of missing assemblies, which
        /// can be viewed.
        /// </summary>
        /// <param name="message">The error, which has the assembly in the message</param>
        internal virtual void RaiseMissingAssembly(string message)
        {
            Contract.Requires(!String.IsNullOrEmpty(message));

            logger.Info("RaiseMissingAssembly: " + message);

            // Create a new list of assemblies if there isn't one
            if (MissingAssemblies == null)
                MissingAssemblies = new ObservableCollection<string>();

            // Add it to the list of missing assemblies if it does not already
            if (!MissingAssemblies.Contains(message))
                MissingAssemblies.Add(message);

            // Instead of Contract.Ensures(MissingAssemblies.Count > 0);
            if (MissingAssemblies.Count == 0)
                throw new SessionApplicationException("RaiseMissingAssembly was called, but nothing was added the list");
        }

        /// <summary>
        /// Adds the error message to the list currently being displayed.  It will only show up to _numberOfErrorsToDisplay
        /// error messages, discarding the oldest one
        /// </summary>
        /// <param name="message">The message to add.</param>
        internal virtual void AddErrorMessage(string message)
        {
            logger.Info("AddErrorMessage: " + message);
            if (_errors == null)
            {
                _errors = new List<string>();
                // Tell the view to display the error row
                RaisePropertyChanged("ErrorRowHeight");
            }

            // Get rid of the last error
            if (_errors.Count == _numberOfErrorsToDisplay)
                _errors.RemoveAt(_numberOfErrorsToDisplay - 1);

            // Add the new one
            _errors.Add(message);

            // Tell the view to update the text
            RaisePropertyChanged("ErrorText");
        }

        /// <summary>
        /// We need the name of the assembly we are missing.  The only way is to parse the message
        /// that was raised.  This is brittle, but I don't know of another way
        /// </summary>
        /// <param name="messageIn">The message in.</param>
        /// <returns></returns>
        private static string GetAssemblyName(string messageIn)
        {
            // Make sure we have a message with an open/close single quote
            if (String.IsNullOrEmpty(messageIn) || (messageIn.Count(c => c == '\'') != 2))
                return null;

            // Extract the full name of the assembly from something like:
            // Unable to find assembly 'TestDataGenerator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
            int start = messageIn.IndexOf('\'') + 1;
            int end = messageIn.IndexOf('\'', start);
            if (end < start)
                return null;

            return messageIn.Substring(start, end - start);
        }

        /// <summary>
        /// Will open the window to the references
        /// </summary>
        private void References()
        {
            logger.Debug("Opening references window");
            Messenger.Default.Send(new MainViewModelMessage(MainViewModelMessage.MessageAction.OpenReferencesView));
        }

        /// <summary>
        /// Will refresh the data on the screen
        /// </summary>
        private void Refresh()
        {
            try
            {
                PopulateSessions();
            }
            catch (Exception ex)
            {
                Helper.HandleException(logger, "There was an error retrieving the session data from the database", ex);
            }
        }

        /// <summary>
        /// Opens the dialog to change the connection string
        /// </summary>
        private void ChangeDatabaseConnection()
        {
            logger.Debug("Opening database connection window");
            Messenger.Default.Send(new MainViewModelMessage(MainViewModelMessage.MessageAction.OpenChangeDatabaseConnectionDialog));

            if (_sessionRepository == null)
                ShutdownApplication();
        }

        /// <summary>
        /// Applies the new application settings to the application.
        /// </summary>
        /// <param name="newSettings">The new settings.</param>
        private void ApplyNewApplicationSettings(ApplicationOptionsChangedMessage newSettings)
        {
            Contract.Requires(newSettings != null);
            Contract.Requires(newSettings.NewSettings != null);

            // Save the new settings in memory
            Options = newSettings.NewSettings;
            // Refresh the session list based on the new settings
            Sessions = new ObservableCollection<SessionData>(_sessionRepository.GetCurrentSessions(newSettings.NewSettings));
        }

        /// <summary>
        /// Message to control the busy indicator
        /// </summary>
        /// <param name="msg">The message with the info on how to set the indicator</param>
        public void BusyIndicatorMessageReceived(BusyIndicatorMessage msg)
        {
            if (!string.IsNullOrEmpty(msg.Message))
            {
                logger.Debug("BusyIndicatorMessageReceived: {0} - [{1}]", msg.IsBusy, msg.Message);
                IsBusyMessage = msg.Message;
            }
            else
            {
                logger.Debug("BusyIndicatorMessageReceived: {0}", msg.IsBusy);
            }
            IsBusy = msg.IsBusy;
        }

        /// <summary>
        /// Gets or sets the options.
        /// </summary>
        /// <value>
        /// The options.
        /// </value>
        public ApplicationOptions Options
        {
            get
            {
                return _options;
            }
            set
            {
                _options = value;
                RaisePropertyChanged("Options");
            }
        }



        /// <summary>
        /// Gets the current reference manager.
        /// </summary>
        public ReferenceManager ReferenceManager
        {
            get
            {
                return _referenceManager;
            }
        }

        /// <summary>
        /// Gets the connection string repository.
        /// </summary>
        public ConnectionStringRepository ConnectionStringRepository
        {
            get
            {
                return _connectionStringRepository;
            }
        }

        /// <summary>
        /// Gets or sets the list of missing assemblies.
        /// </summary>
        /// <value>
        /// A list of assemblies that cannot be resolved when trying to deserialize the data
        /// </value>
        public ObservableCollection<string> MissingAssemblies
        {
            get
            {
                return _missingAssemblies;
            }
            set
            {
                _missingAssemblies = value;
                RaisePropertyChanged("MissingAssemblies");
            }
        }

        /// <summary>
        /// Defines the columns based on current options.
        /// </summary>
        /// <param name="sessionList">The list view of sessions.</param>
        public void DefineColumns(ListView sessionList)
        {
            Contract.Requires(sessionList != null, "Define columns was passed a null list control");

            var view = sessionList.View as GridView;
            if (view == null)
                throw new SessionApplicationException("Define columns was passed something besides a GridView");

            // Add the columns that are always there
            ListViewHelper.DefineSingleColumn(view, true, Strings.Column_SessionId, "SessionId");
            ListViewHelper.DefineSingleColumn(view, true, Strings.Column_Size, "Size");

            var showCreateExpires = Options.ShowCreateExpires;
            var showLockInfo = Options.ShowLockInfo;
            var showApplication = Options.Application;

            // Add the optional columns
            ListViewHelper.DefineSingleColumn(view, showCreateExpires, Strings.Column_Created, "Created");
            ListViewHelper.DefineSingleColumn(view, showCreateExpires, Strings.Column_Expires, "Expires");
            ListViewHelper.DefineSingleColumn(view, showLockInfo, Strings.Column_LockDate, "LockDate");
            ListViewHelper.DefineSingleColumn(view, showLockInfo, Strings.Column_LockDateLocal, "LockDateLocal");
            ListViewHelper.DefineSingleColumn(view, showLockInfo, Strings.Column_LockTimeout, "Timeout");
            ListViewHelper.DefineSingleColumn(view, showLockInfo, Strings.Column_Locked, "Locked");
            ListViewHelper.DefineSingleColumn(view, showApplication, Strings.Column_Application, "Application");
        }

        /// <summary>
        /// Broadcasts the application options change to any listeners.
        /// </summary>
        public void BroadcastApplicationOptionsChange()
        {
            _applicationRepository.BroadcastChange();
        }

        /// <summary>
        /// Unregisters this instance from the Messenger class.
        /// <para>To cleanup additional resources, override this method, clean
        /// up and then call base.Cleanup().</para>
        /// </summary>
        public override void Cleanup()
        {
            Messenger.Default.Unregister<ApplicationOptionsChangedMessage>(this);
            base.Cleanup();
        }
    }
}